iT邦幫忙

2024 iThome 鐵人賽

DAY 12
0
自我挑戰組

30 天 vueuse 原始碼閱讀與實作系列 第 12

[Day 12] useMouse - 加入其他參數 & 功能

  • 分享至 

  • xImage
  •  

今天繼續實作 useMouse,讓他的功能更完整,可以搭配 Day 11 的基本功能實作一起看~

加入 touch option

昨天有提到,useMouse 流程的開始是透過 useEventListener,所以昨天的實作透過 useEventListener 監聽 mousemove、dragover 事件。現在要加入 touch event,一樣從這邊開始

// src/compositions/useMouse.js

const UseMouseBuiltinExtractors = {
  page: event => [event.pageX, event.pageY],
  client: event => [event.clientX, event.clientY],
  screen: event => [event.screenX, event.screenY],
}

export function useMouse(options = {}) {
    const {
        type = 'page',
        touch = true,
        initialValue = { x: 0, y: 0 },
        window = defaultWindow,
        target = window,
    } = options
    
    const x = ref(initialValue.x)
    const y = ref(initialValue.y)
    
    const extractor = typeof type === 'function'
        ? type
        : UseMouseBuiltinExtractors[type]


    const touchHandler = (event) => {
        if (event.touches.length > 0) {
          const result = extractor(event.touches[0])
          if (result) {
            [x.value, y.value] = result
          }
        }
    }

    const touchHandlerWrapper = event => touchHandler(event)

    if (target) {
        const listenerOptions = { passive: true }
        // 加入 touch event
        if (touch && type !== 'movement') {
              useEventListener(target, ['touchstart', 'touchmove'], touchHandlerWrapper, listenerOptions)
        }
    }
}

touch 是 useMouse 的其中一個參數,可以決定要不要監聽 touchstart、touchmove 事件,if (touch && type !== 'movement') 有特別排除 movement,因為 movement 是屬於 mouseEvent 的屬性,並且在非 mousemove 事件的值都會是 0。
movement mdn:https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/movementX

在 touchstart、touchmove 事件觸發後,會執行 event => touchHandler(event),接下來看到 touchHandler 這段,跟昨天講到的 mouseHandler 差不多,到這邊 touch event 功能就完成了。

新增回傳值 - sourceType

先前實作的 useMouse 回傳值是 x、y,看官網 Demo 可以看到還有一個 source type,剛剛加入 touch event 後,可以透過回傳的 source type 讓上層使用者知道目前是觸發 mouse 或是 touch。

// src/compositions/useMouse.js

export function useMouse(options = {}) {
    // ...略
    const sourceType = ref(null)

    const mouseHandler = (event) => {
        // ...略
        if (result) {
          sourceType.value = 'mouse'
        }
    }
    
    const touchHandler = (event) => {
        // ...略
        if (result) {
            sourceType.value = 'touch'
        }
    }
}

加入 resetOnTouchEnds option

這個參數官網文件好像沒有特別提到,如果傳入 true 的話,會在 touchend 事件觸發時,把 x, y 值設定成另一個參數 initialValue 的值,initialValue 沒有傳的話,預設是 { x: 0, y: 0 }

// src/compositions/useMouse.js

export function useMouse(options = {}) {
    const {
        type = 'page',
        touch = true,
        initialValue = { x: 0, y: 0 },
        resetOnTouchEnds = false, // <- 加參數
        window = defaultWindow,
        target = window,
    } = options
    
    // ...略

    const reset = () => {
        x.value = initialValue.x
        y.value = initialValue.y
    }

    // ...略

    if (touch && type !== 'movement') {
        useEventListener(target, ['touchstart', 'touchmove'], touchHandlerWrapper, listenerOptions)
        if (resetOnTouchEnds) { // <- 加在這邊
            useEventListener(target, 'touchend', reset, listenerOptions)
        }
    }
}

加入 scroll option

目前還有個問題,就是頁面在滾動的時候,x、y 座標並沒有更新
有個 PR 處理這個 issue:https://github.com/vueuse/vueuse/pull/3244

像 PR 中說明的,這個功能只針對 target 是 window 同時 type 為 page 的情境。

// src/compositions/useMouse.js

// ...略

let _prevMouseEvent = null

const mouseHandler = (event) => {
    // ... 略
    _prevMouseEvent = event
    
    // ...略
}

const scrollHandler = () => {
    if (!_prevMouseEvent || !window)
        return
    const pos = extractor(_prevMouseEvent)

    if (_prevMouseEvent instanceof MouseEvent && pos) {
        // 以 document 左上角為原點(0, 0),所以鼠標當前位置要加上滾動的距離
        x.value = pos[0] + window.scrollX
        y.value = pos[1] + window.scrollY
    }
}

// ...略
const scrollHandlerWrapper = event => scrollHandler(event)

if (target) {
    const listenerOptions = { passive: true }
    useEventListener(target, ['mousemove', 'dragover'], mouseHandlerWrapper, listenerOptions)
    if (touch && type !== 'movement') {
        useEventListener(target, ['touchstart', 'touchmove'], touchHandlerWrapper, listenerOptions)
    }
    if (scroll && type === 'page') // <- 加在這
        useEventListener(window, 'scroll', scrollHandlerWrapper, listenerOptions)
    }

以上實作的 GitHub PR:https://github.com/RhinoLee/30days_vue/pull/2/files#diff-028451d2fc166c9a050ed78fcfebe2712e1457dad45a9f7e6e8891b8858ec048

加入 eventFilter option

針對三個 event listener wrapper 進行調整,加入 eventFilter 參數後

// src/compositions/useMouse.js
// ...略

const mouseHandlerWrapper = eventFilter
    ? event => eventFilter(() => mouseHandler(event), {})
    : event => mouseHandler(event)

  const touchHandlerWrapper = eventFilter
    ? event => eventFilter(() => touchHandler(event), {})
    : event => touchHandler(event)

  const scrollHandlerWrapper = eventFilter
    ? () => eventFilter(() => scrollHandler(), {})
    : () => scrollHandler()

// ...略

看到這個 eventFilter 似乎有一種熟悉的感覺,其實 Day 2 一開始講到的 useThrottleFn 這個 vueuse API,就有實作到他的核心 throttleFilter,詳細可以從 Day 3 開始看。

也就是說,可以把這個 throttleFilter 當作參數傳給 useMouse,這樣我們事件觸發執行 listener 的時候就可以有 throttle 的效果了!而這些 filter 也可以單獨從 vueuse import 到專案,可以很彈性的做搭配~

參考官方文件:https://vueuse.org/guide/config.html#event-filters
文件是用 throttle 的好兄弟 debounce 當範例。

我的 eventFilter option GitHub PR:https://github.com/RhinoLee/30days_vue/pull/10/files


最後這個 eventFilter 呼應到一開始實作的 throttleFilter,覺得這個設計很棒,useMouse 這樣的功能應該很常會用到這些 filter 來做效能最佳化,沒想到是這樣巧妙的設計~

useMouse 到這邊告一段落,明天會從 useMouseInElement 這個 API 開始講~


上一篇
[Day 11] useMouse - 基本功能
下一篇
[Day 13] useMouseInElement
系列文
30 天 vueuse 原始碼閱讀與實作13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言